namespace Angelus.Data.Linq.OracleClient
{
	using System;
	using System.Collections;
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using System.Data;
	using System.Data.Common;
    using System.Data.DLinq;
    using System.Data.DLinq.Eval;
	using System.Data.DLinq.Provider;
	using System.Data.DLinq.ProviderBase;
	using System.Data.DLinq.SqlClient;
	using System.Data.OracleClient;
	using System.Data.SqlClient;
	using System.Expressions;
	using System.IO;
	using System.Query;
	using System.Reflection;
	using System.Runtime.InteropServices;
	using System.Text;
	using System.Transactions;
	using Angelus.Data.Linq.ProviderBase;

    /// <summary>
    /// 
    /// </summary>
    public class OracleContext : IDBContext, IMaterializationEventRaiser, IConnectionUser
    {
        #region Events

        public event EventHandler<MaterializationEventArgs> EntityMaterialized;
        public event EventHandler<MaterializationEventArgs> EntityMaterializing;
        public event TransactionCompletedEventHandler TransactionCompleted;

        #endregion

        #region Fields

        private readonly bool _checkQueries;
        private readonly SqlEval _eval;
        private readonly SqlFactory _factory;
        private readonly OptimizationFlags _optimizationFlags;
        private readonly TypeSystemProvider _typeProvider;
        private bool _enableCacheLookup;
        private bool _localTransactionsOnly;
        private TextWriter _log;
        private OracleConnectionManager _mgr;
        private int _queryCount;
        private DataServices _services;
        private Translator _translator;

        #endregion

        #region Constructors

        /// <summary>
        /// 
        /// </summary>
        public OracleContext()
        {
            this._optimizationFlags = OptimizationFlags.All;
            this._enableCacheLookup = true;
            this._typeProvider = new OracleTypeSystemProvider();
            this._factory = new SqlFactory(this._typeProvider);
            this._eval = new SqlEval(new EvalFactory());
            this._checkQueries = false;
        }

        #endregion

        #region Properties

        /// <summary>
        /// 
        /// </summary>
        internal IConnectionManager ConnectionManager
        {
            get { return this._mgr; }
        }

        /// <summary>
        /// 
        /// </summary>
        public int QueryCount
        {
            get { return this._queryCount; }
            set { this._queryCount = value; }
        }

        /// <summary>
        /// 
        /// </summary>
        internal DataServices Services
        {
            get { return this._services; }
        }

        /// <summary>
        /// 
        /// </summary>
        public bool EnableCacheLookup
        {
            get { return this._enableCacheLookup; }
            set { this._enableCacheLookup = value; }
        }

        #endregion

        #region IConnectionUser Members

        /// <summary>
        /// 
        /// </summary>
        void IConnectionUser.CompleteUse()
        {
            // Nothing to do
        }

        #endregion

        #region Private Implementation

        /// <summary>
        /// 
        /// </summary>
        /// <param name="query"></param>
        /// <param name="annotations"></param>
        /// <returns></returns>
        internal BuildResult BuildQuery(Expression query, SqlNodeAnnotations annotations)
        {
            SqlNode node =
                new OracleQueryConverter(this._services, this._eval, this._typeProvider, this._translator, this._factory)
                    .ConvertOuter(query);
            return this.BuildQuery(query.Type, node, annotations);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="resultType"></param>
        /// <param name="node"></param>
        /// <param name="annotations"></param>
        /// <returns></returns>
        internal BuildResult BuildQuery(Type resultType, SqlNode node, SqlNodeAnnotations annotations)
        {
            BuildResult result;
            SqlSupersetValidator validator = new SqlSupersetValidator();
            if (this._checkQueries)
            {
                validator.AddValidator(new ColumnTypeValidator());
                validator.AddValidator(new LiteralValidator());
                validator.AddValidator(new ClrBoolIsBitOrBooleanExpression(this._typeProvider));
            }
            validator.Validate(node);
            node = new SqlVbMethodCallConverter(this._factory, this._eval).Visit(node);
            node = new OracleMethodCallConverter(this._factory, this._eval).Visit(node);
            if (node is SqlSelect)
            {
                SqlQuerySet set = new SqlQuerySet(node.SourceExpression);
                set.Queries.Add(node);
                node = set;
                validator.Validate(node);
            }

            node = SqlStaticTreatResolver.Resolve(node, this._typeProvider);
            SqlBinder binder = new SqlBinder(this._translator, this._typeProvider);
            // OracleBinder binder = new OracleBinder(this._translator, this._typeProvider);
            binder.OptimizeLinkExpansions = (this._optimizationFlags & OptimizationFlags.OptimizeLinkExpansions) !=
                                            OptimizationFlags.None;
            binder.SimplifyCaseStatements = (this._optimizationFlags & OptimizationFlags.SimplifyCaseStatements) !=
                                            OptimizationFlags.None;
            node = binder.Bind(node);
            validator.Validate(node);
            if (this._checkQueries)
            {
                validator.AddValidator(new ExpectNoSubExpressions());
            }

            node = new SqlRetyper(this._typeProvider).Retype(node);
            validator.Validate(node);

            node = new OracleMultiplexer(binder, this._typeProvider).Multiplex(node);
            validator.Validate(node);

            node = new SqlReorderer(this._typeProvider).Reorder(node);
            validator.Validate(node);

            node = SqlBooleanizer.Rationalize(node, this._typeProvider);
            if (this._checkQueries)
            {
                validator.AddValidator(new ExpectNoSubExpressions());
            }
            validator.Validate(node);
            node = new OracleFlattener().Flatten(node);
            validator.Validate(node);
            if (this._checkQueries)
            {
                validator.AddValidator(new ExpectNoFloatingColumns());
            }

            SqlAliaser aliaser = new SqlAliaser();
            node = aliaser.AssociateColumnsWithAliases(node);
            validator.Validate(node);

            node = SqlLiftWhereClauses.Lift(node, this._typeProvider);
            node = SqlLiftIndependentRowExpressions.Lift(node);
            node = SqlOuterApplyReducer.Reduce(node, this._factory, annotations);

            node = new SqlResolver().Resolve(node);
            validator.Validate(node);

            node = aliaser.AssociateColumnsWithAliases(node);
            validator.Validate(node);

            node = SqlCaseSimplifier.Simplify(node);
            node = SqlRemoveConstantOrderBy.Remove(node);
            node = new OracleDeflator().Deflate(node);
            validator.Validate(node);

            node = SqlCrossApplyToCrossJoin.Reduce(node, annotations);

            node = new SqlNamer().AssignNames(node);
            validator.Validate(node);

            OracleParameterizer parameterizer = new OracleParameterizer(this._eval);
            parameterizer.Parameterize(node);
            validator.AddValidator(new ExpectNoMethodCalls());
            validator.AddValidator(new ValidateNoInvalidComparison());
            validator.Validate(node);

            ReadOnlyCollection<ReadOnlyCollection<SqlParameterInfo>> parameters = parameterizer.GetParameters();
            string[] queries = new OracleFormatter().Format(node);
            if (node is SqlQuerySet)
            {
                SqlQuerySet local = (SqlQuerySet) node;
                result.Queries = new QueryInfo[queries.Length];
                for (int index = 0, max = queries.Length; index < max; index++)
                {
                    result.Queries[index].Query = local.Queries[index];
                    result.Queries[index].CommandText = queries[index];
                    result.Queries[index].Parameters = parameters[index];
                }
                result.primaryQuery = 0;
                node = local.Queries[0];
            }
            else
            {
                result.Queries = new QueryInfo[1];
                result.Queries[0].Query = node;
                result.Queries[0].CommandText = queries[0];
                result.Queries[0].Parameters = parameters[0];
                result.primaryQuery = 0;
            }

            ReaderBuilder builder = new ReaderBuilder(this._services, this, this._eval, this._translator);
            result.Reader = null;
            if (typeof (void) != resultType)
            {
                SqlSelect select = node as SqlSelect;
                if ((null == select) && (SqlNodeType.Block == node.NodeType))
                {
                    select = this.GetFinalSelect(node);
                }
                if (null != select)
                {
                    Type type = TypeSystem.GetElementType(resultType);
                    result.Reader = builder.BuildReaderForType(select.Selection, type);
                    return result;
                }
                SqlUserQuery query = node as SqlUserQuery;
                if ((null != query) && (null != query.Projection))
                {
                    result.Reader = builder.BuildReader(query.Projection);
                }
            }
            return result;
        }

        /// <summary>
        /// 
        /// </summary>
        private void CheckInitialized()
        {
            if (null == this._services)
            {
                throw new Exception("OracleContext is not initialized");
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="connectionString"></param>
        /// <returns></returns>
        private static DbConnectionStringBuilder GetConnectionStringBuilder(string connectionString)
        {
            DbConnectionStringBuilder builder = new DbConnectionStringBuilder();
            if (connectionString.IndexOf('=') >= 0)
            {
                builder.ConnectionString = connectionString;
            }
            if (!(builder.ContainsKey("enlist")))
            {
                builder.Add("enlist", "false");
            }
            return builder;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        private SqlSelect GetFinalSelect(SqlNode node)
        {
            SqlNodeType type = node.NodeType;
            if (SqlNodeType.Block != type)
            {
                if (SqlNodeType.Select == type)
                {
                    return (SqlSelect) node;
                }
                if (SqlNodeType.Cursor == type)
                {
                    return ((OracleRefCursor) node).Select;
                }
                return null;
            }
            SqlBlock block = (SqlBlock) node;
            return this.GetFinalSelect(block.Statements[block.Statements.Count - 1]);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="args"></param>
        internal void RaiseTransactionCompleted(TransactionEventArgs args)
        {
            this.TransactionCompleted(this, args);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="rowType"></param>
        /// <param name="reader"></param>
        /// <returns></returns>
        internal IEnumerator Translate(MetaType rowType, RowReader reader)
        {
            if (null == rowType)
            {
                throw new ArgumentNullException("rowType");
            }

            SqlNodeAnnotations annotations = new SqlNodeAnnotations();
            Expression expression = Expression.Constant(null);
            SqlUserQuery query = new SqlUserQuery(string.Empty, null, null, expression);
            SqlUserRow row = new SqlUserRow(rowType, this._typeProvider.GetApplicationType(1), query, expression);

            query.Projection = this._translator.BuildDefaultProjection(row, rowType,
                                                                       rowType.IsEntity
                                                                           ? rowType.GetPersistentDataMembers()
                                                                           : rowType.GetDataMembers(), true, expression);
            BuildResult result = this.BuildQuery(typeof (IEnumerable<>).MakeGenericType(new Type[] {rowType.Type}),
                                                 query, annotations);
            Type type = typeof (OracleQuerySession<>).MakeGenericType(new Type[] {rowType.Type});
            OracleQuerySession session =
                (OracleQuerySession)
                Activator.CreateInstance(type, BindingFlags.NonPublic | BindingFlags.Instance, null,
                                         new object[] {this, result.Reader, reader}, null);
            return session.ExecuteQuery();
        }

        #endregion

        #region Nested classes

        #region Nested type: BuildResult

        [StructLayout(LayoutKind.Sequential)]
        internal struct BuildResult
        {
            internal ResultReader Reader;
            internal QueryInfo[] Queries;
            internal int primaryQuery;
        }

        #endregion

        #region Nested type: SingleResult

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        private class SingleResult<T> : IEnumerable<T>
        {
            #region Fields

            private readonly IEnumerable<T> _sequence;

            #endregion

            #region Constructors

            /// <summary>
            /// 
            /// </summary>
            /// <param name="value"></param>
            internal SingleResult(T value)
            {
                this._sequence = new T[] {value};
            }

            #endregion

            #region IEnumerable<T> Members

            /// <summary>
            /// 
            /// </summary>
            /// <returns></returns>
            IEnumerator<T> IEnumerable<T>.GetEnumerator()
            {
                return this._sequence.GetEnumerator();
            }

            /// <summary>
            /// 
            /// </summary>
            /// <returns></returns>
            IEnumerator IEnumerable.GetEnumerator()
            {
                return this._sequence.GetEnumerator();
            }

            #endregion
        }

        #endregion

        #endregion

        #region IDBContext Members

        /// <summary>
        /// 
        /// </summary>
        public IDbConnection Connection
        {
            get
            {
                this.CheckInitialized();
                return this._mgr.Connection;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public void CreateDatabase()
        {
            throw new Exception("The method or operation is not implemented.");
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public bool DatabaseExists()
        {
            throw new Exception("The method or operation is not implemented.");
        }

        /// <summary>
        /// 
        /// </summary>
        public void DeleteDatabase()
        {
            throw new Exception("The method or operation is not implemented.");
        }

        /// <summary>
        /// 
        /// </summary>
        public void DetachDatabase()
        {
            throw new Exception("The method or operation is not implemented.");
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        public IEnumerator Execute(Expression query)
        {
            this.CheckInitialized();
            if (this.EnableCacheLookup)
            {
                object cached = this._services.GetCachedObject(query);
                if (null != cached)
                {
                    Type type = typeof (SingleResult<>).MakeGenericType(new Type[] {cached.GetType()});
                    return
                        ((IEnumerable)
                         Activator.CreateInstance(type, BindingFlags.NonPublic | BindingFlags.Instance, null,
                                                  new object[] {cached}, null)).GetEnumerator();
                }
            }
            SqlNodeAnnotations annotations = new SqlNodeAnnotations();
            BuildResult result = this.BuildQuery(query, annotations);

            Type sessionType =
                typeof (OracleQuerySession<>).MakeGenericType(new Type[] {TypeSystem.GetElementType(query.Type)});
            OracleQuerySession session =
                (OracleQuerySession)
                Activator.CreateInstance(sessionType, BindingFlags.NonPublic | BindingFlags.Instance, null,
                                         new object[] {this, query, result.Queries, result.Reader}, null);
            return session.ExecuteQuery();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        public int ExecuteNonQuery(Expression query)
        {
            this.CheckInitialized();
            SqlNodeAnnotations annotations = new SqlNodeAnnotations();
            BuildResult result = this.BuildQuery(query, annotations);

            OracleQuerySession session = new OracleQuerySession<int>(this, query, result.Queries,
                                                                     (ResultReader<int>) result.Reader);
            return session.ExecuteNonQuery();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="methodInfo"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public StoredProcedureResult ExecuteStoredProcedure(MethodInfo methodInfo, params object[] parameters)
        {
            if (null == methodInfo)
            {
                throw new ArgumentNullException("methodInfo");
            }
            if (null == parameters)
            {
                throw new ArgumentNullException("parameters");
            }

            StoredProcedureResult result;
            MetaFunction function = this._services.Model.GetFunction(methodInfo);
            if (null == function)
            {
                throw new InvalidOperationException("The method specified is not mapped to a stored procedure");
            }

            try
            {
                OracleCommand command = new OracleCommand();
                command.CommandText = function.MappedName;
                command.CommandType = CommandType.StoredProcedure;
                ParameterInfo[] infos = methodInfo.GetParameters();
                if (infos.Length != parameters.Length)
                {
                    throw new ArgumentException("Wrong number of parameters.", "parameters");
                }

                OracleParameter returnParameter = new OracleParameter("RETURN_VALUE", OracleType.Number);
                returnParameter.Direction = ParameterDirection.ReturnValue;
                command.Parameters.Add(returnParameter);

                #region Add Parameters

                MetaParameter[] metaParameters = function.GetParameters().ToArray();
                for (int index = 0; index < metaParameters.Length; index++)
                {
                    OracleParameter parameter = new OracleParameter();
                    MetaParameter metaParameter = metaParameters[index];
                    ParameterInfo info = infos[index];

                    parameter.ParameterName = metaParameter.MappedName;
                    Type parameterType = info.ParameterType;
                    if (parameterType.IsByRef)
                    {
                        parameterType = parameterType.GetElementType();
                        parameter.Direction = ParameterDirection.InputOutput;
                    }
                    if (SqlBuilder.IsNullable(parameterType))
                    {
                        parameterType = parameterType.GetGenericArguments()[0];
                    }

                    object value = parameters[index];
                    if ((null != value) && (parameterType != value.GetType()))
                    {
                        throw new ArgumentException("One or more parameter type mismatches.", "parameters");
                    }

                    string oracleType = metaParameter.DBType;
                    OracleProviderType type = !string.IsNullOrEmpty(oracleType)
                                                  ? OracleProviderType.Parse(oracleType)
                                                  : OracleProviderType.From(parameterType);

                    parameter.OracleType = type.OracleType;
                    parameter.Size = type.Size;
                    Type clrType = type.GetClosestRuntimeType();
                    if (null != value)
                    {
                        parameter.Value = (parameterType != clrType) ? Convert.ChangeType(value, clrType) : value;
                    }
                    else
                    {
                        parameter.Value = DBNull.Value;
                    }
                    command.Parameters.Add(parameter);
                }

                #endregion

                bool hasReturnType = false;
                Type returnType = methodInfo.ReturnType;
                Type methodType = null;

                if (typeof (IEnumerable).IsAssignableFrom(returnType) && returnType.IsGenericType)
                {
                    Type argumentType = returnType.GetGenericArguments()[0];
                    Type genericType = typeof (OracleStoredProcedureResult<>).MakeGenericType(new Type[] {argumentType});
                    if (returnType.IsAssignableFrom(genericType))
                    {
                        methodType = genericType;
                        hasReturnType = true;
                    }
                }
                else if (returnType.IsAssignableFrom(typeof (OracleStoredProcedureMultipleResult)))
                {
                    methodType = typeof (OracleStoredProcedureMultipleResult);
                    hasReturnType = true;
                }
                else if (returnType.IsAssignableFrom(typeof (int)))
                {
                    methodType = typeof (OracleStoredProcedureResult);
                }

                if (null == methodType)
                {
                    string errorText =
                        string.Format("'{0}' is not a valid return type for a mapped stored procedure method.",
                                      returnType.Name);
                    throw new SystemException(errorText);
                }

                StoredProcedureResult dummyResult =
                    Activator.CreateInstance(methodType, BindingFlags.NonPublic | BindingFlags.Instance, null,
                                             new object[] {this, command, function}, null) as StoredProcedureResult;
                result = dummyResult;
                if (!hasReturnType)
                {
                    ((OracleStoredProcedureResult) dummyResult).ExecuteStoredProcedure();
                }
            }
            finally
            {
                this._mgr.ReleaseConnection(this);
            }
            return result;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        public string GetQueryText(Expression query)
        {
            this.CheckInitialized();

            SqlNodeAnnotations annotations = new SqlNodeAnnotations();
            BuildResult result = this.BuildQuery(query, annotations);
            StringBuilder sb = new StringBuilder();

            for (int index = 0, count = result.Queries.Length; index < count; index++)
            {
                if (index > 0)
                {
                    sb.AppendLine();
                    if (count > 1)
                    {
                        sb.AppendLine();
                    }
                }
                sb.Append(result.Queries[index].CommandText);
            }

            return sb.ToString();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="dataServices"></param>
        /// <param name="connection"></param>
        void IDBContext.Initialize(DataServices dataServices, object connection)
        {
            if (null == dataServices)
            {
                throw new ArgumentNullException("dataServices");
            }

            this._services = dataServices;

            OracleTransaction transaction = null;
            OracleConnection database;

            string connectionString = connection as string;
            if (null != connectionString)
            {
                DbConnectionStringBuilder builder = GetConnectionStringBuilder(connectionString);
                database = new OracleConnection();
                database.ConnectionString = builder.ConnectionString;
            }
            else
            {
                transaction = connection as OracleTransaction;
                if (null != transaction)
                {
                    connection = transaction.Connection;
                }
                database = connection as OracleConnection;
                if (null == database)
                {
                    throw new ArgumentNullException("connection must be string, OracleConnection or OracleTransaction");
                }
            }

            this._mgr = new OracleConnectionManager(this, database, 1);
            if (null != transaction)
            {
                this._mgr.LocalTransaction = transaction;
            }
            this._translator = new Translator(this._services, this._factory, this._typeProvider);
        }

        /// <summary>
        /// 
        /// </summary>
        public IDbTransaction LocalTransaction
        {
            get
            {
                this.CheckInitialized();
                return this._mgr.LocalTransaction;
            }
            set
            {
                this.CheckInitialized();
                this._mgr.LocalTransaction = value;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public bool LocalTransactionsOnly
        {
            get
            {
                this.CheckInitialized();
                return this._localTransactionsOnly;
            }
            set
            {
                this.CheckInitialized();
                this._localTransactionsOnly = value;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public TextWriter Log
        {
            get
            {
                this.CheckInitialized();
                return this._log;
            }
            set
            {
                this.CheckInitialized();
                this._log = value;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="elementType"></param>
        /// <param name="reader"></param>
        /// <returns></returns>
        public IEnumerator Translate(Type elementType, IDataReader reader)
        {
            this.CheckInitialized();
            MetaTable table = this._services.Model.GetTable(elementType);
            MetaType type = (null != table)
                                ? table.RowType.GetInheritanceType(elementType)
                                : this._services.Model.GetUnmappedType(elementType);
            OracleCompletingRowReader rowReader = new OracleCompletingRowReader(this._mgr, new DataRowReader(reader));
            this._mgr.UseConnection(rowReader);
            return Translate(type, rowReader);
        }

        #endregion

        #region IMaterializationEventRaiser Members

        /// <summary>
        /// 
        /// </summary>
        /// <param name="args"></param>
        void IMaterializationEventRaiser.RaiseEntityMaterialized(MaterializationEventArgs args)
        {
            this.EntityMaterialized(this, args);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="args"></param>
        void IMaterializationEventRaiser.RaiseEntityMaterializing(MaterializationEventArgs args)
        {
            this.EntityMaterializing(this, args);
        }

        #endregion
    }
}